package com.cnbm.wcsbasic.protocol.netty.s7.s7connector.enmuc;


import com.cnbm.wcsbasic.protocol.netty.s7.retry.S7RetryTemplate;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.api.DaveArea;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.api.S7Connector;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.api.factory.S7ConnectorFactory;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.api.utils.ByteUtils;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.exception.S7CheckResultException;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.exception.S7Exception;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.type.PlcVar;
import com.cnbm.wcsbasic.protocol.netty.s7.s7connector.utils.CommonFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;


/**
 * @Desc: ""
 * @Author: caixiang
 * @DATE: 2022/1/15 13:01
 */
public enum S7Client {
    //TODO 步骤1  这里是配置多PLC 的，，，有多个plc 就在这里配置一个枚举类
    //1500    西门子200smart、1200、1500默认的 机架号=0  槽位号=1;  300/400 默认的  机架-0  插槽-2
//    S7_1200("192.168.0.52",0,1,1,PlcVarActual.HeartBeatFor1200),
//    S7_15001("192.168.0.51",0,1,1),
//    S7_1500("192.168.0.51",0,1,1),


    S7_DDJ1("172.22.24.150",0,1,2),
    S7_DDJ2("172.22.24.160",0,1,2),
    S7_DDJ3("172.22.24.170",0,1,2),
    S7_DDJ4("172.22.24.180",0,1,2),

    S7_Line1("172.22.24.190",0,1,2),
    S7_Line2("172.22.24.195",0,1,2),

    S7_JXS("172.22.23.10",0,1,2),

    //1500  机架-0  插槽-1
    //后续 在这里扩展 多PLC应用。
    ;
    private String host;
    //默认 0   机架号
    private Integer rack;
    //默认 0
    private Integer slot;

    //心跳变量，如果plc没有让电控的人加一个，这个是必填的
    private PlcVarActual heartBeat;

    private List<S7Connector> connections;
    //coreSize 是线程池的大小
    private Integer coreSize;
    //pickOne 就是一个初始化 的轮询 取余值
    private int pickOne;
    private static final Logger logger = LoggerFactory.getLogger(S7Client.class);




    //coreSize 是线程池的数量
    S7Client(String host, Integer rack, Integer slot, Integer coreSize){
        this.host = host;
        this.rack = rack;
        this.slot = slot;
        this.pickOne = 0;

        this.coreSize = coreSize;

        connections = new ArrayList<>();
        //connectionPool();

    }

    public String getHost(){
        return this.host;
    }

    /**
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Boolean
     * LREAL      ===>      Double
     * REAL       ===>      Float
     * DATE       ===>      String(yyyy-MM-dd 这种形式的类型)
     * DTL        ===>      String("年-月-日-工作日-时-分-秒" 这种格式)
     * TIME       ===>      Integer(单位 ms)
     * USINT      ===>      Integer
     * SINT       ===>      Integer
     * UINT       ===>      Integer
     * INT        ===>      Integer
     * DINT       ===>      Integer
     * UINT       ===>      Long
     * Byte       ===>      Integer(有符号)(默认)
     *                     Integer(无符号)(后续扩展)
     * Char       ===>      Character
     * WChar      ===>      Character
     * String     ===>      String                      （特殊）


     * 数组变量
     * BoolArray      ===>      List<Boolean>
     * ByteArray      ===>      List<Byte>
     * WordArray      ===>      List<Integer>
     * DWordArray     ===>      List<Integer>
     * CharArray      ===>      List<Character>
     * SIntArray      ===>      List<Integer>
     * IntArray       ===>      List<Integer>
     * DIntArray      ===>      List<Integer>
     * UIntArray      ===>      List<Integer>
     * USIntArray     ===>      List<Integer>
     * UDIntArray     ===>      List<Long>
     * StringArray    ===>      String[]        （特殊）
     *
     * 如果返回null，就代表出现了异常，并且尝试了 retryMax 次数，并且尝试重置连接
     *  return
     *           正常情况   ： 返回对应类型的数据
     *           异常情况   ： 返回null
     * */
    public Object read(DaveArea area, Integer areaNumber, Integer byteOffset, Integer bitOffset, Integer length, Integer strSizes, PlcVar type) {
        return S7RetryTemplate.getInstance().execute(
                context -> {
                    try {
                        S7Connector connector = getConnector();
                        if(connector == null){
                            logger.info("host: "+host+ ";   read No connector found , 现在connections pool为空，并且创建connection失败 ");
                            throw new S7CheckResultException("host: "+host+ ";   read No connector found , 现在connections pool为空，并且创建connection失败 ");
                        }
                        //String 类型比较特殊。 String[] 也是同理，Sring数组里面的子项 也是有两个字节的 readBytes。
                        if(type.equals(PlcVar.STRING)){
                            Integer readBytes = 2;
                            byte[] read = connector.read(
                                    area,
                                    areaNumber,
                                    readBytes,
                                    byteOffset,
                                    bitOffset,
                                    type.getTransportSize()
                            );

                            Integer allLength = Integer.valueOf(read[1])+2;
                            byte[] readF = connector.read(
                                    area,
                                    areaNumber,
                                    allLength,
                                    byteOffset,
                                    bitOffset,
                                    type.getTransportSize()
                            );
                            return type.toObject(readF);
                        }else if(type.equals(PlcVar.BOOL_Array)){
                            byte[] read = connector.read(
                                    area,
                                    areaNumber,
                                    CommonFunctions.exactDivision(length,8),
                                    byteOffset,
                                    bitOffset,
                                    type.getTransportSize()
                            );
                            List<Boolean> booleans = ByteUtils.toBoolArray(read);
                            List<Boolean> res = new ArrayList<Boolean>();
                            for(int i=0;i<length;i++){
                                res.add(i,booleans.get(i));
                            }
                            return res;
                        }else if(type.equals(PlcVar.STRING_Array)){
                            Integer arrayLength =  length;
                            Integer strSize =  strSizes;

                            byte[] read = connector.read(
                                    area,
                                    areaNumber,
                                    arrayLength*(strSize+2),
                                    byteOffset,
                                    bitOffset,
                                    type.getTransportSize()
                            );
                            return ByteUtils.toStrArray(read, arrayLength, strSize);
                        }else {
                            Integer readBytes = type.getTransportSize().getSizeInBytes() * length;
                            byte[] read = connector.read(
                                    area,
                                    areaNumber,
                                    readBytes,
                                    byteOffset,
                                    bitOffset,
                                    type.getTransportSize()
                            );
                            return type.toObject(read);
                        }
                    }catch (Throwable e) {
                        clearAll();
                        e.printStackTrace();
                        logger.info("host: "+host+ ";read 失败；, 目前尝试次数: "+context.getRetryCount()+",errMsg:"+e.getMessage());
                        throw new S7CheckResultException("read errMsg : "+e.getMessage());
                    }

                },
                context -> {
                    logger.info("host: "+host+ ";   S7-Retry : 已达到最大重试次数: "+S7RetryTemplate.getMaxRetryTimes()+", 现在尝试重新连接;");
                    clearAll();
                    return null;
                }
        );
    }
    public Object read(DaveArea area, Integer areaNumber, Integer byteOffset, Integer bitOffset, Integer length, Integer strSizes, PlcVar type,Boolean isTry) {
        try {
            S7Connector connector = getConnector();
            if(connector == null){
                logger.info("host: "+host+ ";   read No connector found , 现在connections pool为空，并且创建connection失败 ");
                throw new S7CheckResultException("host: "+host+ ";   read No connector found , 现在connections pool为空，并且创建connection失败 ");
            }
            //String 类型比较特殊。 String[] 也是同理，Sring数组里面的子项 也是有两个字节的 readBytes。
            if(type.equals(PlcVar.STRING)){
                Integer readBytes = 2;
                byte[] read = connector.read(
                        area,
                        areaNumber,
                        readBytes,
                        byteOffset,
                        bitOffset,
                        type.getTransportSize()
                );

                Integer allLength = Integer.valueOf(read[1])+2;
                byte[] readF = connector.read(
                        area,
                        areaNumber,
                        allLength,
                        byteOffset,
                        bitOffset,
                        type.getTransportSize()
                );
                return type.toObject(readF);
            }else if(type.equals(PlcVar.BOOL_Array)){
                byte[] read = connector.read(
                        area,
                        areaNumber,
                        CommonFunctions.exactDivision(length,8),
                        byteOffset,
                        bitOffset,
                        type.getTransportSize()
                );
                List<Boolean> booleans = ByteUtils.toBoolArray(read);
                List<Boolean> res = new ArrayList<Boolean>();
                for(int i=0;i<length;i++){
                    res.add(i,booleans.get(i));
                }
                return res;
            }else if(type.equals(PlcVar.STRING_Array)){
                Integer arrayLength =  length;
                Integer strSize =  strSizes;

                byte[] read = connector.read(
                        area,
                        areaNumber,
                        arrayLength*(strSize+2),
                        byteOffset,
                        bitOffset,
                        type.getTransportSize()
                );
                return ByteUtils.toStrArray(read, arrayLength, strSize);
            }else {
                Integer readBytes = type.getTransportSize().getSizeInBytes() * length;
                byte[] read = connector.read(
                        area,
                        areaNumber,
                        readBytes,
                        byteOffset,
                        bitOffset,
                        type.getTransportSize()
                );
                return type.toObject(read);
            }
        }catch (Throwable e) {
            clearAll();
            e.printStackTrace();
            throw new S7CheckResultException("read errMsg : "+e.getMessage());
        }
    }


    /**
     *
     * eg :
     *      Object newValue = Boolean.FALSE
     *      s7Service.write(PlcVarActual.HeartBeat, newValue, S7Client.S7_1200);
     *
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Object newValue = Boolean.FALSE
     * LREAL      ===>      Object newValue = Boolean.FALSE
     * REAL       ===>      Object newValue = Boolean.FALSE
     * DATE       ===>      暂时没需求（有问题找我）
     * DTL        ===>      暂时没需求（有问题找我）
     * TIME       ===>      暂时没需求（有问题找我）
     * USINT      ===>      Object newValue = new Integer(1)
     * SINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * INT        ===>      Object newValue = new Integer(1)
     * DINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * Byte       ===>      Object newValue = 0x11
     *
     * Char       ===>      Object newValue = 'a'
     * WChar      ===>      Object newValue = '菜'
     * String     ===>      Object newValue = '你好啊'      （特殊）


     * 数组变量
     *      注意：在write的时候，你write的数量  一定要和  plc中存在的数量一一对应
     * BoolArray      ===>      boolean[] booleanArray = new boolean[2];  .... 赋予值
     * ByteArray      ===>      byte[] write_byteArrays = new byte[2];
     * WordArray      ===>      short[] shortArrays_content = new short[2];
     * DWordArray     ===>      int[] intArrays_content = new int[2];
     * CharArray      ===>      char[] charArrays_content = new char[2];
     * SIntArray      ===>      int[] sintArrays_content = new int[2];
     * IntArray       ===>      int[] iintArrays_content = new int[2];
     * DIntArray      ===>      int[] dintArrays_content = new int[2];
     * UIntArray      ===>      int[] uintArrays_content = new int[3];
     * USIntArray     ===>      int[] usintArrays_content = new int[3];
     * UDIntArray     ===>      int[] udintArrays_content = new int[3];
     * StringArray    ===>      String[] stringArrays_content = new String[3];
     * //如果有其他数据类型  这里没有找我扩展
     *
     *
     *  return
     *           正常情况   ： 不返回值
     *           异常情况   ： 抛出异常中断程序
     * */
    public void write(DaveArea area, Integer areaNumber, Integer byteOffset, Integer bitOffset,Integer strSize, PlcVar type, Object newValue)  {
        S7RetryTemplate.getInstance().execute(
               context -> {
                   try {
                       S7Connector connector = getConnector();
                       if(connector == null){
                           logger.info("host: "+host+ ";   write No connector found , 现在connections pool为空，并且创建connection失败 ");
                           throw new S7CheckResultException("host: "+host+ ";   read No connector found , 现在connections pool为空，并且创建connection失败 ");
                       }
                       //String 类型比较特殊。 String[] 也是同理，Sring数组里面的子项 也是有两个字节的 readBytes。
                       if(type.equals(PlcVar.STRING)){
                           connector.write(
                                   area,
                                   areaNumber,
                                   byteOffset,
                                   bitOffset,
                                   ByteUtils.strToBytes(newValue.toString(), strSize),
                                   type
                           );
                       }else if(type.equals(PlcVar.BOOL_Array)){
                           connector.write(
                                   area,
                                   areaNumber,
                                   byteOffset,
                                   bitOffset,
                                   ByteUtils.toByteArray((boolean[])newValue),
                                   type
                           );
                       }else if(type.equals(PlcVar.STRING_Array)){
                           //todo here 检查 read write service
                           connector.write(
                                   area,
                                   areaNumber,
                                   byteOffset,
                                   bitOffset,
                                   ByteUtils.strArrayToBytes((String[])newValue, strSize),
                                   type
                           );
                       }else {
                           byte[] bytes = type.toBytes(newValue);
                           connector.write(
                                   area,
                                   areaNumber,
                                   byteOffset,
                                   bitOffset,
                                   bytes,
                                   type
                           );
                       }
                       return null;
                   }catch (Throwable e) {
                       clearAll();
                       e.printStackTrace();
                       logger.info("host: "+host+ ";write 失败；, 目前尝试次数: "+context.getRetryCount()+",errMsg:"+e.getMessage());
                       throw new S7CheckResultException("host: "+host+";   write errMsg : "+e.getMessage());
                   }
               },
               context -> {
                   logger.info("host: "+host+ ";  S7-Retry-Write : 已达到最大重试次数: "+S7RetryTemplate.getMaxRetryTimes()+", 现在尝试重新连接");
                   clearAll();
                   throw new S7Exception("host: "+host+";   write 写入异常");
               }
        );
    }



    public synchronized void clearAll(){
        connections.clear();
    }
    public synchronized Integer clientPoolSize(){
        return connections.size();
    }

    //虽然是枚举类，但这个是对象锁（不是类锁）
    public synchronized S7Connector getConnector() {
        int size = connections.size();
        if(size == 0){
            S7Connector connect = connect(host, rack, slot);
            if(connect == null){
                return null;
            }else {
                connections.add(connect);
                return connect;
            }
        }

        S7Connector s7Connector = connections.get((pickOne + size) % size);
        pickOne+=1;
        pickOne = (pickOne)%size;
        return s7Connector;
    }



    private synchronized S7Connector connect(String host,Integer rack,Integer slot ){
        try {
            S7Connector connector = S7ConnectorFactory
                    .buildTCPConnector()
                    .withHost(host)
                    .withRack(rack) //optional   rack 是机架号
                    .withSlot(slot) //optional   slot 是插槽号
                    .build();
            return connector;
        }catch (S7Exception e){
            logger.info("host: "+host+ ";   创建S7Connector  连接失败,原因："+e.getMessage());
//            throw new S7CheckResultException("errMsg : "+e.getMessage());
            return null;
        }

    }
}